﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
using System.Security.Permissions;

namespace FGrubert.VpcManager
{
    /// <summary>
    /// Represents a single virtual machine
    /// </summary>
    public class Machine : IDisposable
    {
        private FileSystemWatcher watcher;

        private string name;
        /// <summary>
        /// Gets or sets the name of the machine
        /// </summary>
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        private string filename;
        /// <summary>
        /// Gets the filename of the machine
        /// </summary>
        public string Filename
        {
            get { return filename; }
        }

        private List<Nic> nics;
        /// <summary>
        /// Gets the nics of the machine
        /// </summary>
        public List<Nic> Nics
        {
            get { return nics; }
        }

        private int ramSize;
        /// <summary>
        /// Gets or sets the size of memory in MB
        /// </summary>
        public int RamSize
        {
            get { return ramSize; }
            set { ramSize = value; }
        }

        private EnumState state;
        /// <summary>
        /// Gets the state of the machine
        /// </summary>
        public EnumState State
        {
            get { return state; }
        }

        /// <summary>
        /// Event that indicates that the information changed
        /// </summary>
        public event DlgMachineInformationChanged Changed;

        /// <summary>
        /// Creates a new machine object and reads the data from the file
        /// </summary>
        /// <param name="filename">The filename to the configuration</param>
        public Machine(string filename)
        {
            this.name = Path.GetFileNameWithoutExtension(filename);
            this.filename = filename;

            XmlDocument doc = new XmlDocument();
            try
            {
                using (Stream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    doc.Load(stream);
                }
            }
            catch (IOException)
            {
                // Usually this occurs when the write process is not finished, so we wait 1.5 seconds and 
                // try it again
                DateTime start = DateTime.Now;
                while ((DateTime.Now - start).TotalSeconds < 1.5) ;

                using (Stream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    doc.Load(stream);
                }
            }

            UpdateMachineInformation(doc);

            InitializeWatcher();
        }

        /// <summary>
        /// Initializes the FileSystemWatcher for monitoring changes
        /// </summary>
        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        private void InitializeWatcher()
        {
            this.watcher = new FileSystemWatcher(Path.GetDirectoryName(this.filename), Path.GetFileName(this.filename));
            this.watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;

            this.watcher.Renamed += new RenamedEventHandler(watcher_Renamed);
            this.watcher.Changed += new FileSystemEventHandler(watcher_Changed);

            this.watcher.EnableRaisingEvents = true;
        }

        
        /// <summary>
        /// Raised when the name of the virtual machine changes
        /// </summary>
        private void watcher_Renamed(object sender, RenamedEventArgs e)
        {
            this.filename = e.FullPath;
            this.name = Path.GetFileNameWithoutExtension(this.filename);

            if (this.Changed != null)
            {
                this.Changed(this);
            }
        }

        /// <summary>
        /// The file was changed so update the machine information
        /// </summary>
        /// <param name="sender">The watcher that raised the event</param>
        /// <param name="e">Additional information about the change</param>
        private void watcher_Changed(object sender, FileSystemEventArgs e)
        {
            try
            {
                XmlDocument doc = new XmlDocument();
                using (Stream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    doc.Load(stream);
                }

                UpdateMachineInformation(doc);

                if (this.Changed != null)
                {
                    this.Changed(this);
                }
            }
            catch
            {
                // Sometimes we have som errors because VPC is no finished writing the file so we catch this 
                // kind of errors as next flush will start this method again
            }
        }

        /// <summary>
        /// Updates the information about the machine according to the xml document
        /// </summary>
        /// <param name="doc">The xml document holding the machine information</param>
        private void UpdateMachineInformation(XmlDocument doc)
        {
            this.ramSize = Convert.ToInt32(doc.SelectSingleNode("//preferences/hardware/memory/ram_size").InnerText);

            XmlNode node = doc.SelectSingleNode("//preferences/settings/shutdown/quit/was_running");
            if (node != null && Convert.ToBoolean(node.InnerText))
            {
                this.state = EnumState.Running;
            }
            else
            {
                node = doc.SelectSingleNode("//preferences/settings/configuration/saved_state/path/relative");
                if (node != null && !string.IsNullOrEmpty(node.InnerText))
                {
                    this.state = EnumState.Saved;
                }
                else
                {
                    this.state = EnumState.NotRunning;
                }
            }

            XmlNodeList nodes = doc.SelectNodes("//preferences/hardware/pci_bus/ethernet_adapter/ethernet_controller");
            this.nics = new List<Nic>(nodes.Count);
            foreach (XmlNode nicNode in nodes)
            {
                nics.Add(new Nic(this, nicNode));
            }
        }

        /// <summary>
        /// Starts this machine
        /// </summary>
        public void StartMachine()
        {
            System.Diagnostics.Process.Start(this.filename);
        }

        /// <summary>
        /// Resets the MAC address of the given NIC
        /// </summary>
        /// <param name="nicId">The id of the NIC</param>
        public void ResetNicMac(int nicId)
        {
            // This action is only valid if the machine is not running
            if (this.state != EnumState.NotRunning)
            {
                return;
            }

            XmlDocument doc = new XmlDocument();
            doc.PreserveWhitespace = true;
            using (Stream stream = File.Open(this.filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                doc.Load(stream);
            }

            XmlNode node = doc.SelectSingleNode("//preferences/hardware/pci_bus/ethernet_adapter/ethernet_controller[@id='" + nicId + "']/ethernet_card_address");
            if (node != null)
            {
                node.InnerXml = "";
                XmlWriterSettings settings = new XmlWriterSettings();
                settings.Encoding = Encoding.Unicode;
                settings.CloseOutput = true;
                using (XmlTextWriter writer = new XmlTextWriter(filename, Encoding.Unicode))
                {
                    doc.Save(writer);
                }
            }
            UpdateMachineInformation(doc);

            if (this.Changed != null)
            {
                Changed(this);
            }
        }

        #region IDisposable Member

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            if (this.watcher != null)
            {
                this.watcher.Dispose();
            }
        }

        #endregion
    }
}
